"""
=============
ballista.core
=============

This module contains the core classes and methods of Ballista.

"""

import cgi
import re
import os

import ballista.config
from ballista.exceptions import BallistaException, IncompleteResponse, InvalidRoute, NoMatchingRoute, UnexpectedResponse
from ballista.responses import Response

PREFIX = ballista.config.MAPPER_VARIABLE_PREFIX

class ApplicationHandler:
    """
    ApplicationHandler
    ==================

    The base application handler superclass, which all individual application handlers must implement.
    
    """
    common_headers = [('Content-Type', 'text/html')]
    controller_prefix = None
    routes = ()
    silent_404 = False
    def __init__(self, config={}, request=None):
        """
        ApplicationHandler.__init__
        ===========================
        
        __init__(self, config={}, request=None) -> ApplicationHandler instance
        Instantiate the handler, commonly with a ``config`` dictionary. If no ``request`` argument is passed, one will be retrieved from cgi.FieldStorage using the value of ``request``, eventually defaulting to 'index'.
        
        """
        self.config = config
        
        request_data = ballista.config.request_data = cgi.FieldStorage()
        if request is None:
            request_source = os.environ.get('BALLISTA_REQUEST_SOURCE')
            if request_source == 'FIELDSTORAGE':
                request = request_data.getvalue('request') or 'index'
                self.request = request
            else:  # we're going to assume we want to use PATH_INFO
                self.request = os.environ.get('PATH_INFO', '')
        else:
            self.request = request

    def __call__(self, environ, start_response):
        """
        ApplicationHandler.__call__
        ===========================

        __call__(self, environ, start_response)
        WSGI wrapper around ``ApplicationHandler.start``
        
        """
        try:
            out = self.start(environ, start_response)
        except BallistaException, e:
            if os.environ.get('BALLISTA_DEBUG'):
                raise e
            else:
                start_response('500 Internal Server Error', [('Content-Type', 'text/html')])
                return [ballista.config.GENERIC_ERROR_PAGE % ('Ballista: Application Error', "An error occured within the application. If you're the developer of this application, you may either browse the log files or turn on debugging mode to fix this error.")]

        return out

    def start(self, environ, start_response):
        """
        ApplicationHandler.start
        ========================

        start(self, environ, start_response)
        Use the ``routes`` attribute to map the request to a controller and an action.
        
        """
        mapper = Mapper()
        for path, directives in self.routes:
            mapper.connect(path, **directives)

        if self.silent_404:
            mapper.set_default(0)

        error = False
        try:
            handler = mapper.match(self.request)
        except NoMatchingRoute:
            error = True
            handler = {'controller': 'errors', 'action': 'e404', 'request': self.request}

        if not 'controller' in handler:
            raise InvalidRoute("The request was mapped to a route which contained no 'controller' value.")
        elif not 'action' in handler:
            raise InvalidRoute("The request was mapped to a route which contained no 'action' value.")

        controller = handler.pop('controller')
        action = handler.pop('action')
        try:
            if self.controller_prefix:
                mp = '%s.%s' % (self.controller_prefix, controller)
                module = __import__(mp)
                if '.' in self.controller_prefix:
                    bits = self.controller_prefix.split('.')[1:]
                    for bit in bits:
                        module = getattr(module, bit)

                module = getattr(module, controller)
            else:
                mp = controller
                module = __import__(mp)

            Controller = getattr(module, '%sController' % controller.capitalize())
        except (ImportError, AttributeError), e:
            if isinstance(e, ImportError):
                raise InvalidRoute("Application config linked to controller '%s', however no such controller module was found (attempted to import %s)." % (controller, mp))
            else:
                raise InvalidRoute("Application config linked to controller '%s', however module contains no controller class (searched for %s)." % (controller, '%s.%sController' % (mp, controller.capitalize())))

        instance = Controller(self.config)
        try:
            run = getattr(instance, action)
        except AttributeError:
            raise InvalidRoute("Application config linked to action '%s' of controller '%s', however the controller contained no such method." % (action, controller))

        headers = self.common_headers
        status = '200 OK'
        out = []

        try:
            responses = run(**handler)
        except Exception, e:
            raise BallistaException('[%s] %s' % (e.__class__, e))

        if isinstance(responses, Response):
            responses = [responses]

        for response in responses:
            if response.headers:
                headers += response.headers
            if response.status:
                status = response.status

        out = response.out()
        start_response(status, headers)
        return [out]

class Controller:
    """
    Controller
    ==========

    The very basic controller superclass.
    
    """
    def __init__(self, config={}):
        """
        Controller.__init__
        ===================

        __init__(self, config={}) -> Controller instance
        Initialise the controller and store the sent ``config`` dictionary.
        
        """
        self.config = config

class Mapper:
    """
    Mapper
    ======

    A very simplified version of the ``Router`` module (since we really don't need all of its power).
    
    """
    routes = []
    def connect(self, url, require={}, **kwargs):
        """
        Mapper.connect
        ==============
        
        connect(self, url, require={}, **kwargs)
        Connect a route, where ``url`` is the plain string and ``require`` specifies requirements for any of the arguments. Any additional keyworded arguments are defaults for the expected variables inside the ``url`` string.
        
        """
        expr = []
        variables = []
        
        parts = url.split('/')
        for part in parts:
            if part.startswith(PREFIX):
                var = part[len(PREFIX):]
                variables.append(var)
                if var in require:
                    expr.append('(%s)' % require[var])
                else:
                    expr.append('([^/]+)')
            else:
                expr.append(re.escape(part))

        self.routes.append(('^%s$' % '/'.join(expr), variables, kwargs))

    def set_default(self, no):
        """
        Mapper.set_default
        ==================

        set_default(self, no)
        Define the default route as the routed registered as number ``no``.
        
        """
        self.default = self.routes[no]
        if self.default[1]:
            raise InvalidRoute('The default route cannot take any variable arguments.')

    def match(self, url):
        """
        Mapper.match
        ============

        match(self, url) -> dict
        Match ``url`` against the connected routes, returning a dictionary of all the gathered variable values.
        
        """
        url = '/'.join(part for part in url.split('/') if part)
        for expr, variables, defaults in self.routes:
            match = re.compile(expr).match(url)
            if match:
                defaults.update(dict(zip(variables, match.groups())))
                return defaults
        else:
            try:
                args = self.default[2]
            except AttributeError:
                raise NoMatchingRoute("No route matching '%s' was found." % url)
            else:
                return args
